// Package description contains objects to describe streams.
package description

import (
	"fmt"
	"reflect"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"unicode"

	"github.com/pion/ice/v4"
	psdp "github.com/pion/sdp/v3"
	"gogs.smarteye.com/smarteye/gortc/pkg/format"
)

var (
	smartPayloadTypeRegexp = regexp.MustCompile("^smart/[0-9]/[0-9]+$")
	smartRtpmapRegexp      = regexp.MustCompile("^([0-9]+) (.+)/[0-9]+$")
)

func replaceSmartPayloadType(payloadType string, attributes []psdp.Attribute) string {
	re1 := smartPayloadTypeRegexp.FindStringSubmatch(payloadType)
	if re1 != nil {
		for _, attr := range attributes {
			if attr.Key == "rtpmap" {
				re2 := smartRtpmapRegexp.FindStringSubmatch(attr.Value)
				if re2 != nil {
					return re2[1]
				}
			}
		}
	}
	return payloadType
}

func getAttribute(attributes []psdp.Attribute, key string) string {
	for _, attr := range attributes {
		if attr.Key == key {
			return attr.Value
		}
	}
	return ""
}

func getDirection(attributes []psdp.Attribute) (psdp.Direction, bool) {
	for _, attr := range attributes {
		if direction, err := psdp.NewDirection(attr.Key); err == nil {
			return direction, true
		}
	}
	return psdp.DirectionInactive, false
}

func getFormatAttribute(attributes []psdp.Attribute, payloadType uint8, key string) string {
	for _, attr := range attributes {
		if attr.Key == key {
			v := strings.TrimSpace(attr.Value)
			if parts := strings.SplitN(v, " ", 2); len(parts) == 2 {
				if tmp, err := strconv.ParseUint(parts[0], 10, 8); err == nil && uint8(tmp) == payloadType {
					return parts[1]
				}
			}
		}
	}
	return ""
}

func decodeFMTP(enc string) map[string]string {
	if enc == "" {
		return nil
	}

	ret := make(map[string]string)

	for _, kv := range strings.Split(enc, ";") {
		kv = strings.Trim(kv, " ")

		if len(kv) == 0 {
			continue
		}

		tmp := strings.SplitN(kv, "=", 2)
		if len(tmp) != 2 {
			continue
		}

		ret[strings.ToLower(tmp[0])] = tmp[1]
	}

	return ret
}

func sortedKeys(fmtp map[string]string) []string {
	keys := make([]string, len(fmtp))
	i := 0
	for key := range fmtp {
		keys[i] = key
		i++
	}
	sort.Strings(keys)
	return keys
}

func isAlphaNumeric(v string) bool {
	for _, r := range v {
		if !unicode.IsLetter(r) && !unicode.IsNumber(r) {
			return false
		}
	}
	return true
}

// MediaType is the type of a media stream.
type MediaType string

// media types.
const (
	MediaTypeVideo       MediaType = "video"
	MediaTypeAudio       MediaType = "audio"
	MediaTypeApplication MediaType = "application"
	MediaTypeText        MediaType = "text"
	MediaTypeMessage     MediaType = "message"
)

// Media is a media stream.
// It contains one or more formats.
type Media struct {
	// Media type.
	Type MediaType

	// mid Media ID (optional).
	ID string

	Direction psdp.Direction

	BvSSRC, SSRC              uint32
	CName, StreamLabel, Label string

	Candidate ice.Candidate // read-only

	Protos []string // 默认值(["RTP", "AVP"], ["TCP", "RTP", "AVP"]) 可选值: "UDP", "RTP", "AVP", "SAVP", "SAVPF", "TLS", "DTLS", "SCTP", "AVPF", "TCP", "MSRP"

	// Formats contained into the media.
	Formats []format.Format
}

// Unmarshal decodes the media from the SDP format.
func (m *Media) Unmarshal(md *psdp.MediaDescription) error {
	m.Type = MediaType(md.MediaName.Media)
	m.Protos = md.MediaName.Protos

	m.ID = getAttribute(md.Attributes, "mid")
	if m.ID != "" && !isAlphaNumeric(m.ID) {
		return fmt.Errorf("invalid mid: %v", m.ID)
	}

	m.Direction, _ = getDirection(md.Attributes)

	m.Formats = nil
	for _, payloadType := range md.MediaName.Formats {
		payloadType = replaceSmartPayloadType(payloadType, md.Attributes)

		tmp, err := strconv.ParseUint(payloadType, 10, 8)
		if err != nil {
			return err
		}
		payloadTypeInt := uint8(tmp)

		rtpMap := getFormatAttribute(md.Attributes, payloadTypeInt, "rtpmap")
		fmtp := decodeFMTP(getFormatAttribute(md.Attributes, payloadTypeInt, "fmtp"))

		formatV, err := format.Unmarshal(string(m.Type), payloadTypeInt, rtpMap, fmtp)
		if err != nil {
			return err
		}

		m.Formats = append(m.Formats, formatV)
	}

	if m.Formats == nil {
		return fmt.Errorf("no formats found")
	}

	if value, ok := md.Attribute(psdp.AttrKeyCandidate); ok {
		if candidate, err := ice.UnmarshalCandidate(value); err == nil {
			m.Candidate = candidate
		}
	}

	if value, ok := md.Attribute(psdp.AttrKeySSRC); ok {
		if parts := strings.Split(strings.TrimSpace(value), " "); len(parts) > 0 {
			if ssrc, err := strconv.Atoi(parts[0]); err == nil {
				m.SSRC = uint32(ssrc)
			}
		}
	}
	if value, ok := md.Attribute("bvssrc"); ok {
		if parts := strings.Split(strings.TrimSpace(value), " "); len(parts) > 0 {
			if ssrc, err := strconv.Atoi(parts[0]); err == nil {
				m.BvSSRC = uint32(ssrc)
			}
		}
	}

	return nil
}

// Marshal encodes the media in SDP format.
func (m Media) Marshal() *psdp.MediaDescription {
	protos := m.Protos
	if len(protos) == 0 {
		protos = []string{"RTP", "AVP"}
	}

	md := &psdp.MediaDescription{
		MediaName: psdp.MediaName{
			Media:  string(m.Type),
			Protos: protos,
		},
		MediaTitle: nil,
		ConnectionInformation: &psdp.ConnectionInformation{
			NetworkType: "IN",
			AddressType: "IP4",
			Address:     &psdp.Address{Address: "0.0.0.0"},
		},
		Bandwidth:     nil,
		EncryptionKey: nil,
		Attributes:    nil,
	}

	if m.ID != "" {
		md.Attributes = append(md.Attributes, psdp.Attribute{
			Key:   "mid",
			Value: m.ID,
		})
	}

	for _, forma := range m.Formats {
		typ := strconv.FormatUint(uint64(forma.PayloadType()), 10)
		md.MediaName.Formats = append(md.MediaName.Formats, typ)

		rtpmap := forma.RTPMap()
		if rtpmap != "" {
			md.Attributes = append(md.Attributes, psdp.Attribute{
				Key:   "rtpmap",
				Value: typ + " " + rtpmap,
			})
		}

		fmtp := forma.FMTP()
		if len(fmtp) != 0 {
			tmp := make([]string, len(fmtp))
			for i, key := range sortedKeys(fmtp) {
				tmp[i] = key + "=" + fmtp[key]
			}

			md.Attributes = append(md.Attributes, psdp.Attribute{
				Key:   "fmtp",
				Value: typ + " " + strings.Join(tmp, "; "),
			})
		}
	}

	md.WithPropertyAttribute(psdp.AttrKeyRTCPMux)
	md.WithPropertyAttribute(psdp.AttrKeyRTCPRsize)
	md.WithValueAttribute(psdp.AttrKeyConnectionSetup, "active") // active/passive

	md.WithPropertyAttribute(m.Direction.String())

	md.WithMediaSource(m.SSRC, m.CName, m.StreamLabel, m.Label)

	return md
}

// FindFormat finds a certain format among all the formats in the media.
func (m Media) FindFormat(forma interface{}) bool {
	for _, formak := range m.Formats {
		if reflect.TypeOf(formak) == reflect.TypeOf(forma).Elem() {
			reflect.ValueOf(forma).Elem().Set(reflect.ValueOf(formak))
			return true
		}
	}
	return false
}
